# Copyright (c) 2019 Presto Labs Pte. Ltd.
# Author: xguo

import base64
import enum
import hashlib
import hmac
import json
import logging
import time
import urllib.parse

import requests
import requests.auth

from coin.base.timestamp import get_timestamp
from coin.exchange.util.async_http_client import AsyncHttpClient
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate

from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.util.rate_limit import RateLimiter

KRAKEN_FUTURES_API_URL = 'futures.kraken.com'
KRAKEN_FUTURES_SANDBOX_API_URL = 'conformance.cryptofacilities.com'


class KrakenFuturesOrderType(enum.Enum):
  LIMIT = 'lmt'
  POST_ONLY = 'post'
  STOP = 'stp'


class KrakenFuturesOrderSide(enum.Enum):
  BUY = 'buy'
  SELL = 'sell'


class KrakenFuturesProductCode(enum.Enum):
  INVERSE_FUTURES = 'FI'
  VANILLA_FUTURES = 'FV'
  PERPETUAL_INVERSE_FUTURES = 'PI'
  PERPETUAL_VANILLA_FUTURES = 'PV'
  REAL_TIME_INDEX = 'IN'
  REFERENCE_RATE = 'RR'


class KrakenFuturesContractType(enum.Enum):
  MONTH = 'month'
  QUARTER = 'quarter'
  PERPETUAL = 'perpetual'


class KrakenFuturesOrderStatus(enum.Enum):
  PARTIALLY_FILLED = 'partiallyFilled'
  UNTOUCHED = 'untouched'


class KrakenFuturesSubmitStatus(enum.Enum):
  UNKNOWN = 'unknown'
  PLACED = 'placed'
  ATTEMPTED = 'attempted'  # post order
  INVALID_ORDER_TYPE = 'invalidOrderType'
  INVALID_SIDE = 'invalidSide'
  INVALID_SIZE = 'invalidSize'
  INVALID_PRICE = 'invalidPrice'
  SELF_FILL = 'selfFill'
  TOO_MANY_SMALL_ORDERS = 'tooManySmallOrders'
  MARKET_SUSPENDED = 'marketSuspended'
  MARKET_INACTIVE = 'marketInactive'
  CLIENT_ORDER_ID_ALREADY_EXIST = 'clientOrderIdAlreadyExist'
  CLIENT_ORDER_ID_TOO_LONG = 'clientOrderIdTooLong'


class KrakenFuturesCancelStatus(enum.Enum):
  UNKNOWN = 'unknown'
  CANCELED = 'cancelled'
  FILLED = 'filled'
  NOT_FOUND = 'notFound'


class KrakenFuturesFillType(enum.Enum):
  TAKER_FILL_TYPE = 'taker'
  MAKER_FILL_TYPE = 'maker'
  LIQUIDATION_FILL_TYPE = 'liquidation'
  ASSIGNEE = 'assignee'
  ASSIGNOR = 'assignor'


class KrakenFuturesErrorType(enum.Enum):
  API_LIMIT_EXCEEDED = 'apiLimitExceeded'
  AUTHENTICATION_ERROR = 'authenticationError'
  ACCOUNT_INACTIVE = 'accountInactive'
  NONCE_BELOW_THRESHOLD = 'nonceBelowThreshold'
  NONCE_DUPLICATE = 'nonceDuplicate'


class KrakenFuturesWsOrderUpdateReason(enum.Enum):
  UNKNOWN = 'unknown'
  NEW_PLACED_ORDER_BY_USER = 'new_placed_order_by_user'
  LIQUIDATION = 'liquidation'
  STOP_ORDER_TRIGGERED = 'stop_order_triggered'
  LIMIT_ORDER_FROM_STOP = 'limit_order_from_stop'
  PARTIAL_FILL = 'partial_fill'
  FULL_FILL = 'full_fill'
  CANCELLED_BY_USER = 'cancelled_by_user'
  CONTRACT_EXPIRED = 'contract_expired'
  NOT_ENOUGH_MARGIN = 'not_enough_margin'
  MARKET_INACTIVE = 'market_inactive'
  CANCELLED_BY_ADMIN = 'cancelled_by_admin'


class KrakenFuturesAuth(requests.auth.AuthBase):
  def __init__(self, key_file):
    auth_key = AuthKey.from_file(key_file)
    self._decoded_secret_key = base64.b64decode(auth_key.secret_key)
    self._auth_key = auth_key
    self._nonce = 0
    self.challenge = None  # For ws authentication

  @property
  def auth_key(self):
    return self._auth_key

  def get_nonce(self):
    nonce = get_timestamp()
    for _ in range(10):
      if self._nonce != nonce:
        nonce = get_timestamp()
        break
    self._nonce = nonce
    return str(nonce)

  def gen_auth(self, message):
    """Use by rest and ws authentication"""
    hash_digest = hashlib.sha256(message.encode()).digest()
    signature = hmac.new(self._decoded_secret_key, hash_digest, hashlib.sha512)
    return base64.b64encode(signature.digest()).decode()

  def __call__(self, req: requests.PreparedRequest):
    parsed_result = urllib.parse.urlparse(req.url)
    endpoint_path = parsed_result.path
    assert endpoint_path.startswith('/derivatives'), endpoint_path
    endpoint_path = endpoint_path[len('/derivatives'):]
    if req.method == 'GET':
      query_params = parsed_result.query
    elif req.method == 'POST':
      if req.body is None:
        query_params = ''
      elif isinstance(req.body, str):
        query_params = req.body
      elif isinstance(req.body, bytes):
        query_params = req.body.decode()
      else:
        raise ValueError('req.body not recognizable! %s', req.body)
    else:
      raise ValueError('Http method not supported! %s', req.method)

    nonce = self.get_nonce()
    concat_str = query_params + nonce + endpoint_path
    signature = self.gen_auth(concat_str)
    auth_header = {
        'APIKey': self._auth_key.access_key,
        'Nonce': nonce,
        'Authent': signature,
    }
    req.headers.update(auth_header)
    return req


class KrakenFuturesSyncQueryer(object):
  def __init__(self, key_file, *, api_host=None, disable_ssl=False, timeout=10):
    self._timeout = timeout
    self._auth = KrakenFuturesAuth(key_file)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or KRAKEN_FUTURES_API_URL)
    self.rate_limit = RateLimiter(50, 10)
    self._logger = logging.getLogger(__name__)

  def auth_request(self, method, path, params):
    assert method in ('GET', 'POST'), method
    data = None
    if method == 'POST':
      data = params
      params = None

    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    try:
      response = requests.request(method,
                                  url,
                                  params=params,
                                  data=data,
                                  auth=self._auth,
                                  timeout=self._timeout)
      content = response.json()
      return content
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException: %s')
      raise
    except json.JSONDecodeError:
      self._logger.exception('Json decode: %s', response.content)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def _query_nocheck(self, method, path, params):
    req_timestamp = time.time()
    content = self.auth_request(method, path, params)
    res_timestamp = time.time()
    status = requests.codes.ok
    update = RestUpdate(req_timestamp, res_timestamp, content, status)
    return update

  def query(self, method, path, params=None, disable_rate_limit=False):
    if params is not None:
      params = [(key, value) for key, value in params if value is not None]
    if not disable_rate_limit:
      while not self.rate_limit.check():
        time.sleep(0.1)
    else:
      self.rate_limit.check()
    return self._query_nocheck(method, path, params)


class KrakenFuturesAsyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=4,
  ):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = KrakenFuturesAuth(key_file)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or KRAKEN_FUTURES_API_URL)

  def query(self, method, path, params=None):
    assert method in ('GET', 'POST'), method
    assert params is None or isinstance(params, list), params
    if params is not None:
      params = [(key, value) for key, value in params if value is not None]

    data = None
    if method == 'POST':
      data = params
      params = None

    url = urllib.parse.urljoin(self._api_host, path)
    future = self._http_client.fetch(method=method,
                                     url=url,
                                     params=params,
                                     data=data,
                                     auth=self._auth,
                                     timeout=self._timeout)
    return future


class KrakenFuturesNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=8,
      use_async=False,
  ):
    if use_async:
      self._auth_queryer = KrakenFuturesAsyncQueryer(key_file,
                                                     api_host=api_host,
                                                     disable_ssl=disable_ssl,
                                                     timeout=timeout,
                                                     max_connections=max_connections)
    else:
      self._auth_queryer = KrakenFuturesSyncQueryer(key_file, api_host=api_host, timeout=timeout)

  def _query(self, method, path, params=None):
    return self._auth_queryer.query(method, path, params=params)

  def place_order(self,
                  *,
                  order_type: KrakenFuturesOrderType,
                  symbol,
                  order_side: KrakenFuturesOrderSide,
                  order_size,
                  limit_price,
                  stop_price=None,
                  client_order_id=None):
    path = '/derivatives/api/v3/sendorder'
    method = 'POST'
    params = [
        ('orderType', order_type.value),
        ('symbol', symbol),
        ('side', order_side.value),
        ('size', order_size),
        ('limitPrice', limit_price),
        ('stopPrice', stop_price),
        ('cliOrdId', client_order_id),
    ]
    update = self._query(method, path, params)
    return update

  def batch_order(self, json_data, batch_order):
    path = '/derivatives/api/v3/batchorder'
    method = 'POST'
    params = [
        ('json', json_data),
        ('batch_order', batch_order),
    ]
    update = self._query(method, path, params)
    return update

  def cancel_order(self, *, order_id, client_order_id=None):
    path = '/derivatives/api/v3/cancelorder'
    method = 'POST'
    params = [
        ('order_id', order_id),
        ('cliOrdId', client_order_id),
    ]
    update = self._query(method, path, params)
    return update

  def cancel_all_orders(self, symbol):
    path = '/derivatives/api/v3/cancelallorders'
    method = 'POST'
    params = [
        ('symbol', symbol),
    ]
    update = self._query(method, path, params)
    return update

  def cancel_all_orders_after(self, timeout):
    path = '/derivatives/api/v3/cancelallordersafter'
    method = 'POST'
    params = [
        ('timeout', timeout),
    ]
    update = self._query(method, path, params)
    return update

  def query_fills(self, last_fill_time=None):
    path = '/derivatives/api/v3/fills'
    method = 'GET'
    params = [
        ('lastFillTime', last_fill_time),
    ]
    update = self._query(method, path, params)
    return update

  def query_open_position(self):
    path = '/derivatives/api/v3/openpositions'
    method = 'GET'
    update = self._query(method, path)
    return update

  def query_open_orders(self):
    path = '/derivatives/api/v3/openorders'
    method = 'GET'
    update = self._query(method, path)
    return update

  def query_accounts(self):
    path = '/derivatives/api/v3/accounts'
    method = 'GET'
    update = self._query(method, path)
    return update
